package net.lgy.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import net.lgy.component.MinIOFileStoreEngine;
import net.lgy.component.StoreEngine;
import net.lgy.config.MinioConfig;
import net.lgy.controller.req.*;
import net.lgy.dto.*;
import net.lgy.enums.BizCodeEnum;
import net.lgy.enums.FileTypeEnum;
import net.lgy.enums.FolderFlagEnum;
import net.lgy.exception.BizException;
import net.lgy.mapper.AccountFileMapper;
import net.lgy.mapper.AccountMapper;
import net.lgy.mapper.FileMapper;
import net.lgy.mapper.StorageMapper;
import net.lgy.model.AccountFileDO;
import net.lgy.model.FileDO;
import net.lgy.model.StorageDO;
import net.lgy.service.AccountFileService;
import net.lgy.util.CommonUtil;
import net.lgy.util.SpringBeanUtil;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


/**
 * 账户文件服务实现类
 * 处理文件和文件夹的核心业务逻辑，包括创建、查询、重命名、移动、复制、删除、上传等操作
 */
@Slf4j
@Service
public class AccountFileServiceImpl implements AccountFileService {

    @Autowired
    private AccountFileMapper accountFileMapper;  // 账户文件关联表数据访问层

    @Resource
    private MinIOFileStoreEngine minIOFileStoreEngine;  // MinIO文件存储引擎

    @Autowired
    private MinioConfig minioConfig;  // MinIO配置信息

    @Autowired
    private FileMapper fileMapper;  // 文件表数据访问层

    @Autowired
    private AccountMapper accountMapper;  // 账户表数据访问层

    @Autowired
    private StorageMapper storageMapper;  // 存储空间表数据访问层

    @Autowired
    private RedissonClient redissonClient;  // Redisson分布式锁客户端

    @Autowired
    private StringRedisTemplate redisTemplate;  // Redis操作模板

    /**
     * 创建文件夹
     * 构建文件夹信息并调用通用方法保存账户文件关联关系
     *
     * @param createRootFolderReq 文件夹创建请求参数（包含账户ID、父文件夹ID、文件夹名称）
     * @return 新创建的文件夹ID
     */
    @Override
    public Long createFolder(FolderCreateReq createRootFolderReq) {
        // 构建账户文件DTO对象（标记为文件夹）
        AccountFileDTO accountFileDTO = AccountFileDTO.builder()
                .accountNo(createRootFolderReq.getAccountNo())
                .parentId(createRootFolderReq.getParentId())
                .fileName(createRootFolderReq.getFolderName())
                .isDir(FolderFlagEnum.YES.getCode())  // 标记为文件夹
                .registerType(createRootFolderReq.getType())
                .build();

        // 保存文件夹信息并返回ID
        return saveAccountFile(accountFileDTO);
    }

    /**
     * 查询指定文件夹下的文件列表
     * 按文件夹/文件类型排序，文件夹在前，按创建时间倒序排列
     *
     * @param accountId  账户ID
     * @param parentId   父文件夹ID（根目录为0）
     * @return 文件夹下的文件/文件夹DTO列表
     */
    @Override
    public List<AccountFileDTO> listFile(Long accountId, Long parentId) {
        // 查询指定账户、指定父文件夹下的所有文件/文件夹
        List<AccountFileDO> accountFileDOList = accountFileMapper.selectList(
                new QueryWrapper<AccountFileDO>()
                        .eq("account_no", accountId)
                        .eq("parent_id", parentId)
                        .orderByDesc("is_dir")  // 文件夹在前，文件在后
                        .orderByDesc("gmt_create")  // 按创建时间倒序
        );

        // 转换为DTO列表并返回
        return SpringBeanUtil.copyProperties(accountFileDOList, AccountFileDTO.class);
    }

    /**
     * 重命名文件或文件夹
     * 1. 校验文件是否存在
     * 2. 校验新名称是否与原名称相同
     * 3. 校验同目录下是否存在同名文件/文件夹
     * 4. 执行重命名操作
     *
     * @param request 重命名请求参数（包含账户ID、文件ID、新名称）
     * @return 重命名成功返回true，否则返回false
     */
    @Override
    public Boolean renameFile(FileUpdateRequest request) {
        // 检查文件是否存在且属于当前用户
        AccountFileDO accountFileDO = accountFileMapper.selectOne(
                new QueryWrapper<AccountFileDO>()
                        .eq("account_no", request.getAccountNo())
                        .eq("id", request.getFileId())
        );
        if (accountFileDO == null) {
            log.info("文件不存在,{}", request);
            throw new BizException(BizCodeEnum.FILE_NOT_EXISTS);
        }

        // 校验新名称是否与原名称相同
        if (accountFileDO.getFileName().equals(request.getNewFileName())) {
            log.info("文件名称相同,{}", request);
            throw new BizException(BizCodeEnum.FILE_RENAME_REPEAT);
        }

        // 校验同目录下是否存在同名文件/文件夹
        Long duplicateCount = accountFileMapper.selectCount(
                new QueryWrapper<AccountFileDO>()
                        .eq("account_no", request.getAccountNo())
                        .eq("parent_id", accountFileDO.getParentId())
                        .eq("file_name", request.getNewFileName())
        );
        if (duplicateCount > 0) {
            log.error("文件名重复,{}", request);
            throw new BizException(BizCodeEnum.FILE_RENAME_REPEAT);
        }

        // 执行重命名更新
        int rows = accountFileMapper.update(
                new UpdateWrapper<AccountFileDO>()
                        .eq("id", accountFileDO.getId())
                        .set("file_name", request.getNewFileName())
        );
        return rows == 1;
    }

    /**
     * 生成文件夹树形结构
     * 1. 查询用户所有文件夹
     * 2. 构建文件夹映射关系
     * 3. 组装树形结构并返回根目录
     *
     * @param accountNo 账户ID
     * @return 文件夹树形结构列表
     */
    @Override
    public List<FolderTreeNodeDTO> folderTree(Long accountNo) {
        // 查询用户所有文件夹
        List<AccountFileDO> folderList = accountFileMapper.selectList(
                new QueryWrapper<AccountFileDO>()
                        .eq("account_no", accountNo)
                        .eq("is_dir", FolderFlagEnum.YES.getCode())
        );

        // 文件夹列表为空时返回空列表
        if (CollectionUtil.isEmpty(folderList)) {
            return List.of();
        }

        // 构建文件夹ID到树形节点的映射
        Map<Long, FolderTreeNodeDTO> folderMap = folderList.stream()
                .collect(Collectors.toMap(
                        AccountFileDO::getId,
                        accountFileDO -> FolderTreeNodeDTO.builder()
                                .id(accountFileDO.getId())
                                .parentId(accountFileDO.getParentId())
                                .label(accountFileDO.getFileName())
                                .children(new ArrayList<>())
                                .build()
                ));

        // 为每个文件夹找到其父文件夹并添加到子节点列表
        for (FolderTreeNodeDTO node : folderMap.values()) {
            Long parentId = node.getParentId();
            if (parentId != 0 && folderMap.containsKey(parentId)) {
                FolderTreeNodeDTO parentNode = folderMap.get(parentId);
                parentNode.getChildren().add(node);
            }
        }

        // 过滤出根目录文件夹（parentId为0）
        List<FolderTreeNodeDTO> rootNodes = folderMap.values().stream()
                .filter(node -> node.getParentId() == 0)
                .toList();

        return rootNodes;
    }

    /**
     * 生成文件夹树形结构（版本2）
     * 与folderTree方法类似，采用不同的树形组装逻辑
     *
     * @param accountNo 账户ID
     * @return 文件夹树形结构列表
     */
    @Override
    public List<FolderTreeNodeDTO> folderTreeV2(Long accountNo) {
        // 查询当前用户的所有文件夹
        List<AccountFileDO> folderList = accountFileMapper.selectList(
                new QueryWrapper<AccountFileDO>()
                        .eq("account_id", accountNo)
                        .eq("is_dir", FolderFlagEnum.YES.getCode())
        );

        // 文件夹列表为空时返回空列表
        if (CollectionUtils.isEmpty(folderList)) {
            return List.of();
        }

        // 转换为树形节点DTO列表
        List<FolderTreeNodeDTO> folderTreeNodeDTOS = folderList.stream().map(file ->
                FolderTreeNodeDTO.builder()
                        .id(file.getId())
                        .label(file.getFileName())
                        .parentId(file.getParentId())
                        .children(new ArrayList<>())
                        .build()
        ).toList();

        // 按父文件夹ID分组，构建子文件夹映射
        Map<Long, List<FolderTreeNodeDTO>> folderTreeNodeVOMap = folderTreeNodeDTOS.stream()
                .collect(Collectors.groupingBy(FolderTreeNodeDTO::getParentId));

        // 为每个节点添加子节点
        for (FolderTreeNodeDTO node : folderTreeNodeDTOS) {
            List<FolderTreeNodeDTO> children = folderTreeNodeVOMap.get(node.getId());
            if (!CollectionUtils.isEmpty(children)) {
                node.getChildren().addAll(children);
            }
        }

        // 过滤出根目录文件夹（parentId为0）
        return folderTreeNodeDTOS.stream()
                .filter(node -> Objects.equals(node.getParentId(), 0L))
                .collect(Collectors.toList());
    }

    /**
     * 小文件上传
     * 1. 上传文件到存储引擎（MinIO）
     * 2. 保存文件信息和账户文件关联关系
     * 3. 处理上传失败的异常情况
     *
     * @param request 文件上传请求参数（包含文件流、账户ID、父文件夹ID等）
     * @return 上传成功返回true，否则返回false
     */
    @Override
    public Boolean fileUpload(FileUploadRequest request) {
        // 上传文件到存储引擎，获取存储路径
        String objectKey = storeFile(request);

        // 保存文件信息和账户文件关联关系
        Boolean result = saveFileAndAccountFile(request, objectKey);
        if (!result) {
            log.error("保存文件关系失败,{}", request);
            throw new BizException(BizCodeEnum.FILE_TYPE_ERROR);
        }
        return result;
    }

    /**
     * 保存文件信息和账户文件关联关系
     * 1. 保存文件元数据到文件表
     * 2. 保存账户与文件的关联关系到账户文件表
     *
     * @param request   文件上传请求参数
     * @param objectKey 文件在存储引擎中的路径
     * @return 保存成功返回true，否则返回false
     */
    public Boolean saveFileAndAccountFile(FileUploadRequest request, String objectKey) {
        // 保存文件元数据
        FileDO fileDO = saveFile(request, objectKey);

        // 保存账户文件关联关系
        if (fileDO != null) {
            AccountFileDTO accountFileDTO = AccountFileDTO.builder()
                    .accountNo(request.getAccountNo())
                    .fileId(fileDO.getId())
                    .fileName(request.getFileName())
                    .fileSuffix(CommonUtil.getFileSuffix(request.getFileName()))  // 提取文件后缀
                    .fileSize(request.getFile() != null ? request.getFile().getSize() : request.getFileSize())
                    .parentId(request.getParentId())
                    .isDir(FolderFlagEnum.NO.getCode())  // 标记为文件
                    .fileType(FileTypeEnum.fromExtension(fileDO.getFileSuffix()).name()) // 确定文件类型
                    .objectKey(objectKey)
                    .build();

            Long fileId = saveAccountFile(accountFileDTO);
            log.info("保存文件关系成功");
            return fileId != null;
        }
        return false;
    }

    /**
     * 保存文件元数据到文件表
     *
     * @param request   文件上传请求参数
     * @param objectKey 文件在存储引擎中的路径
     * @return 保存成功返回FileDO对象，否则返回null
     */
    private FileDO saveFile(FileUploadRequest request, String objectKey) {
        FileDO fileDO = new FileDO();
        fileDO.setFileName(request.getFileName());
        fileDO.setAccountNO(request.getAccountNo());
        fileDO.setFileSize(request.getFile() != null ? request.getFile().getSize() : request.getFileSize());
        fileDO.setFileSuffix(CommonUtil.getFileSuffix(request.getFileName()));
        fileDO.setObjectKey(objectKey);
        fileDO.setIdentifier(request.getIdentifier());  // 文件唯一标识（用于秒传）

        int rows = fileMapper.insert(fileDO);
        return rows == 1 ? fileDO : null;
    }

    /**
     * 上传文件到MinIO存储引擎
     *
     * @param request 文件上传请求参数
     * @return 文件在MinIO中的存储路径
     */
    private String storeFile(FileUploadRequest request) {
        // 生成唯一文件名（避免重复）
        String fileName = CommonUtil.getFilePath(request.getFileName());
        // 上传文件到指定桶
        boolean upload = minIOFileStoreEngine.upload(minioConfig.getBucketName(), fileName, request.getFile());
        return fileName;
    }

    /**
     * 保存账户与文件/文件夹的关联关系
     * 1. 校验父文件夹是否存在
     * 2. 处理文件名重复问题
     * 3. 保存关联关系到数据库
     *
     * @param accountFileDTO 账户文件关联信息DTO
     * @return 保存成功返回记录ID，否则返回null
     */
    private Long saveAccountFile(AccountFileDTO accountFileDTO) {
        // 检查父文件夹是否存在
        if (accountFileDTO.getRegisterType() != 1) {
            Boolean isParentValid = checkParentFile(accountFileDTO);
        }
        // 转换DTO为DO对象
        AccountFileDO accountFileDO = SpringBeanUtil.copyProperties(accountFileDTO, AccountFileDO.class);

        // 处理文件名重复问题
        AccountFileDO processedFileDO = processFileNameDuplicate(accountFileDO);

        // 保存关联关系
        accountFileMapper.insert(processedFileDO);

        return processedFileDO.getId();
    }

    /**
     * 处理文件名重复问题
     * 若同目录下存在同名文件/文件夹，在名称后添加时间戳后缀
     *
     * @param accountFileDO 账户文件关联DO对象
     * @return 处理后的DO对象（名称已去重）
     */
    public AccountFileDO processFileNameDuplicate(AccountFileDO accountFileDO) {
        // 检查同目录下是否存在同名文件/文件夹
        Long duplicateCount = accountFileMapper.selectCount(
                new QueryWrapper<AccountFileDO>()
                        .eq("account_no", accountFileDO.getAccountNo())
                        .eq("parent_id", accountFileDO.getParentId())
                        .eq("is_dir", accountFileDO.getIsDir())
                        .eq("file_name", accountFileDO.getFileName())
        );

        if (duplicateCount > 0) {
            // 处理文件夹重名
            if (accountFileDO.getIsDir().equals(FolderFlagEnum.YES.getCode())) {
                accountFileDO.setFileName(accountFileDO.getFileName() + "_" + System.currentTimeMillis());
            }
            // 处理文件重名（保留扩展名）
            else {
                String[] fileNameParts = accountFileDO.getFileName().split("\\.");
                accountFileDO.setFileName(
                        fileNameParts[0] + "_" + System.currentTimeMillis() + "." + fileNameParts[1]
                );
            }
        }
        return accountFileDO;
    }

    /**
     * 检查父文件夹是否存在且属于当前用户
     *
     * @param accountFileDTO 账户文件关联DTO对象
     * @return 父文件夹存在返回true，否则抛出异常
     */
    private Boolean checkParentFile(AccountFileDTO accountFileDTO) {
        if (accountFileDTO.getParentId() != null) {
            AccountFileDO accountFileDO = accountFileMapper.selectOne(
                    new QueryWrapper<AccountFileDO>()
                            .eq("id", accountFileDTO.getParentId())
                            .eq("account_no", accountFileDTO.getAccountNo())
            );
            if (accountFileDO == null) {
                throw new BizException(BizCodeEnum.FILE_NOT_EXISTS);
            }
            return true;
        }
        return false;
    }

    /**
     * 批量移动文件/文件夹到目标文件夹
     * 1. 校验待移动文件的合法性
     * 2. 校验目标文件夹的合法性
     * 3. 处理文件名重复问题
     * 4. 执行批量移动操作
     *
     * @param request 批量移动请求参数（包含文件ID列表、目标文件夹ID等）
     * @return 移动成功返回true，否则返回false
     */
    @Override
    public Boolean moveBatch(FileBatchRequest request) {
        // 校验待移动文件是否合法（属于当前用户）
        List<AccountFileDO> accountFileDOList = checkFileIdLegal(request.getFileIds(), request.getAccountNo());

        // 校验目标文件夹是否合法
        checkTargetParentFileLegal(request);

        // 更新待移动文件的父ID为目标文件夹ID
        accountFileDOList.forEach(accountFileDO -> accountFileDO.setParentId(request.getTargetParentId()));

        // 处理目标文件夹中的文件名重复问题
        accountFileDOList.forEach(this::processFileNameDuplicate);

        // 执行批量更新操作
        for (AccountFileDO accountFileDO : accountFileDOList) {
            if (accountFileMapper.updateById(accountFileDO) < 1) {
                throw new BizException(BizCodeEnum.FILE_MOVE_FAIL);
            }
        }

        return true;
    }

    /**
     * 批量删除文件/文件夹
     * 1. 校验待删除文件的合法性
     * 2. 递归获取所有子文件/文件夹
     * 3. 加分布式锁保护存储空间更新
     * 4. 更新存储空间并删除文件关联记录
     *
     * @param request 批量删除请求参数（包含文件ID列表、账户ID等）
     * @return 删除成功返回true，否则返回false
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean deleteBatch(FileDelRequest request) {
        boolean lockReleased = false;
        // 生成分布式锁的唯一键
        String lockKey = "lock:storage:delete:aipan" + request.getAccountNo();

        // 校验待删除文件是否合法（属于当前用户）
        List<AccountFileDO> accountFileDOS = checkFileIdLegal(request.getFileIds(), request.getAccountNo());

        // 递归获取所有子文件/文件夹（包括嵌套的）
        List<AccountFileDO> allFiles = new ArrayList<>();
        findAllAccountFileDOWithRecur(allFiles, accountFileDOS, false);

        // 提取所有待删除文件的ID
        List<Long> allFileIds = allFiles.stream().map(AccountFileDO::getId).toList();

        // 计算待删除文件的总大小（仅文件，不包括文件夹）
        long totalFileSize = allFiles.stream()
                .filter(file -> file.getIsDir().equals(FolderFlagEnum.NO.getCode()))
                .mapToLong(AccountFileDO::getFileSize)
                .sum();

        // 查询当前用户的存储空间信息
        StorageDO storageDO = storageMapper.selectOne(
                new QueryWrapper<StorageDO>().eq("account_no", request.getAccountNo())
        );

        // 获取分布式锁，防止并发更新存储空间
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试加锁，最多等待2秒，5秒后自动解锁
            lockReleased = lock.tryLock(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        // 更新存储空间（减去删除的文件大小）
        storageDO.setUsedSize(storageDO.getUsedSize() - totalFileSize);
        storageMapper.updateById(storageDO);

        // 批量删除账户文件关联记录
        int rows = accountFileMapper.deleteBatchIds(allFileIds);




        // 释放分布式锁
        if (lockReleased) {
            redisTemplate.delete(lockKey);
        }

        return rows > 0 && lockReleased;
    }

    /**
     * 检查存储空间是否充足并更新使用量
     *
     * @param accountNo 账户ID
     * @param fileSize  待添加的文件总大小
     * @return 空间充足并更新成功返回true，否则返回false
     */
    @Override
    public boolean checkAndUpdateCapacity(Long accountNo, Long fileSize) {
        // 查询当前用户的存储空间信息
        StorageDO storageDO = storageMapper.selectOne(
                new QueryWrapper<StorageDO>().eq("account_no", accountNo)
        );

        // 检查空间是否充足
        if (storageDO.getTotalSize() <= storageDO.getUsedSize() + fileSize) {
            // 存储空间不足
            return false;
        }

        // 更新已用空间
        storageDO.setUsedSize(storageDO.getUsedSize() + fileSize);
        storageMapper.updateById(storageDO);
        return true;
    }

    /**
     * 校验目标文件夹的合法性
     * 1. 目标必须是文件夹
     * 2. 不能将文件移动到自身或子文件夹中
     *
     * @param request 批量移动请求参数
     */
    private void checkTargetParentFileLegal(FileBatchRequest request) {
        // 校验目标是否为文件夹
        AccountFileDO targetFile = accountFileMapper.selectOne(
                new QueryWrapper<AccountFileDO>()
                        .eq("id", request.getTargetParentId())
                        .eq("is_dir", FolderFlagEnum.NO.getCode())  // 目标是文件则不合法
                        .eq("account_no", request.getAccountNo())
        );
        if (targetFile != null) {
            log.error("目标文件id不合法，不是文件夹");
            throw new BizException(BizCodeEnum.FILE_MOVE_FAIL);
        }

        // 查询待移动的文件/文件夹
        List<AccountFileDO> prepareFiles = accountFileMapper.selectList(
                new QueryWrapper<AccountFileDO>()
                        .eq("account_no", request.getAccountNo())
                        .in("id", request.getFileIds())
        );

        // 递归获取所有待移动的文件/文件夹（包括子文件）
        List<AccountFileDO> allFiles = new ArrayList<>();
        findAllAccountFileDOWithRecur(allFiles, prepareFiles, false);

        // 校验目标文件夹是否在待移动的文件列表中（防止循环移动）
        if (allFiles.stream().anyMatch(file -> file.getId().equals(request.getTargetParentId()))) {
            log.error("目标文件id不合法，不能移动到该文件下");
            throw new BizException(BizCodeEnum.FILE_MOVE_FAIL);
        }
    }

    /**
     * 递归获取所有文件/文件夹（包括子文件/子文件夹）
     *
     * @param resultList     存储结果的列表
     * @param prepareFiles   待处理的文件/文件夹列表
     * @param onlyFolder     是否只收集文件夹
     */
    public void findAllAccountFileDOWithRecur(List<AccountFileDO> resultList,
                                              List<AccountFileDO> prepareFiles,
                                              boolean onlyFolder) {
        for (AccountFileDO file : prepareFiles) {
            // 如果是文件夹，递归处理其子文件
            if (file.getIsDir().equals(FolderFlagEnum.YES.getCode())) {
                List<AccountFileDO> childFiles = accountFileMapper.selectList(
                        new QueryWrapper<AccountFileDO>().eq("parent_id", file.getId())
                );
                findAllAccountFileDOWithRecur(resultList, childFiles, onlyFolder);
            }

            // 根据条件添加到结果列表（onlyFolder为true时只添加文件夹）
            if (!onlyFolder || file.getIsDir().equals(FolderFlagEnum.YES.getCode())) {
                resultList.add(file);
            }
        }
    }


    /**
     * 校验文件ID列表的合法性（是否都属于当前用户）
     *
     * @param fileIds   文件ID列表
     * @param accountNo 账户ID
     * @return 合法的文件DO列表
     */
    public List<AccountFileDO> checkFileIdLegal(List<Long> fileIds, Long accountNo) {
        List<AccountFileDO> files = accountFileMapper.selectList(
                new QueryWrapper<AccountFileDO>()
                        .eq("account_no", accountNo)
                        .in("id", fileIds)
        );

        // 校验查询结果数量是否与请求数量一致
        if (files.size() != fileIds.size()) {
            log.error("文件批量操作失败，数量不合法，ids-{}", fileIds);
            throw new BizException(BizCodeEnum.FILE_MOVE_FAIL);
        }
        return files;
    }

    /**
     * 批量复制文件/文件夹到目标文件夹
     * 1. 校验待复制文件的合法性
     * 2. 校验目标文件夹的合法性
     * 3. 递归复制所有文件/子文件夹
     * 4. 检查并更新存储空间
     * 5. 批量保存复制的文件关联记录
     *
     * @param request 批量复制请求参数
     * @return 复制成功返回true，否则返回false
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean copyBatch(FileBatchRequest request) {
        // 校验待复制文件的合法性
        List<AccountFileDO> files = checkFileIdLegal(request.getFileIds(), request.getAccountNo());

        // 校验目标文件夹的合法性
        checkTargetParentFileLegal(request);

        // 递归复制所有文件/子文件夹，生成新的关联记录
        List<AccountFileDO> newFiles = findBatchCopyFileWithRecur(files, request.getTargetParentId());

        // 计算复制文件的总大小（仅文件）
        long totalSize = newFiles.stream()
                .filter(file -> file.getIsDir().equals(FolderFlagEnum.NO.getCode()))
                .mapToLong(AccountFileDO::getFileSize)
                .sum();

        // 检查存储空间是否充足
        if (!checkAndUpdateCapacity(request.getAccountNo(), totalSize)) {
            throw new BizException(BizCodeEnum.FILE_STORAGE_NOT_ENOUGH);
        }

        // 批量保存复制的文件关联记录
        Object insertResult = accountFileMapper.insertFileBatch(newFiles);

        return true;
    }

    /**
     * 递归复制文件/文件夹（批量处理）
     *
     * @param files           待复制的文件/文件夹列表
     * @param targetParentId  目标父文件夹ID
     * @return 复制后的文件/文件夹DO列表
     */
    public List<AccountFileDO> findBatchCopyFileWithRecur(List<AccountFileDO> files, Long targetParentId) {
        List<AccountFileDO> newFiles = new ArrayList<>();
        // 遍历待复制的文件/文件夹，递归处理
        files.forEach(file -> doCopyChildRecord(newFiles, file, targetParentId));
        return newFiles;
    }

    /**
     * 递归复制单个文件/文件夹及其子文件
     *
     * @param newFiles        存储复制结果的列表
     * @param sourceFile      源文件/文件夹
     * @param targetParentId  目标父文件夹ID
     */
    private void doCopyChildRecord(List<AccountFileDO> newFiles, AccountFileDO sourceFile, Long targetParentId) {
        Long sourceId = sourceFile.getId();  // 保存源文件ID（用于查询子文件）

        // 生成新的ID，更新父ID，重置创建时间
        sourceFile.setId(IdUtil.getSnowflakeNextId());
        sourceFile.setParentId(targetParentId);
        sourceFile.setGmtCreate(null);

        // 处理目标文件夹中的文件名重复问题
        processFileNameDuplicate(sourceFile);

        // 添加到结果列表
        newFiles.add(sourceFile);

        // 如果是文件夹，递归复制其子文件
        if (sourceFile.getIsDir().equals(FolderFlagEnum.YES.getCode())) {
            // 查询源文件夹的子文件
            List<AccountFileDO> childFiles = findChildAccountFileDOS(sourceFile.getAccountNo(), sourceId);
            if (childFiles == null) {
                return;
            }

            // 递归复制子文件，父ID设为当前新文件夹的ID
            childFiles.forEach(childFile -> doCopyChildRecord(newFiles, childFile, sourceFile.getId()));
        }
    }

    /**
     * 查询指定文件夹下的子文件/文件夹
     *
     * @param accountNo 账户ID
     * @param parentId  父文件夹ID
     * @return 子文件/文件夹列表
     */
    private List<AccountFileDO> findChildAccountFileDOS(Long accountNo, Long parentId) {
        return accountFileMapper.selectList(
                new QueryWrapper<AccountFileDO>()
                        .eq("account_no", accountNo)
                        .eq("parent_id", parentId)
        );
    }

    /**
     * 文件秒传功能
     * 1. 根据文件唯一标识查询是否已存在该文件
     * 2. 检查存储空间是否充足
     * 3. 直接创建账户文件关联关系（无需重新上传）
     *
     * @param request 秒传请求参数（包含文件唯一标识、账户ID等）
     * @return 秒传成功返回true，否则返回false
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean SecondBatch(FileSecondUploadRequest request) {
        // 根据文件唯一标识查询是否已存在该文件
        FileDO fileDO = fileMapper.selectOne(
                new QueryWrapper<FileDO>().eq("identifier", request.getIdentifier())
        );

        // 若文件存在且存储空间充足，执行秒传
        if (fileDO != null && checkAndUpdateCapacity(request.getAccountNo(), fileDO.getFileSize())) {
            // 创建账户文件关联关系
            AccountFileDTO accountFileDTO = AccountFileDTO.builder()
                    .accountNo(request.getAccountNo())
                    .fileId(fileDO.getId())
                    .fileName(fileDO.getFileName())
                    .fileSuffix(fileDO.getFileSuffix())
                    .fileSize(fileDO.getFileSize())
                    .isDir(FolderFlagEnum.NO.getCode())  // 标记为文件
                    .parentId(request.getParentId())
                    .del(false)
                    .build();

            // 保存关联关系
            saveAccountFile(accountFileDTO);
            return true;
        }

        // 文件不存在或空间不足，无法秒传
        return false;
    }



    /**
     * 文件名称搜索功能
     * 根据用户账号ID和搜索关键词，查询该用户名下「未删除」且「文件名包含关键词」的文件，
     * 结果按「文件夹优先（降序）」和「创建时间最新（降序）」排序，最多返回30条数据（避免数据量过大）
     *
     * @param accountNo 用户账号ID（用于权限过滤，仅查询当前用户的文件）
     * @param search    搜索关键词（匹配文件名，模糊查询）
     * @return List<AccountFileDTO> 搜索结果列表（DO转DTO后返回，隐藏数据库层字段）
     */
    @Override
    public List<AccountFileDTO> search(Long accountNo, String search) {
        log.info("用户[{}]发起文件搜索，关键词：{}", accountNo, search);

        // 1. 构建查询条件：用户归属+文件名模糊匹配+默认未删除（del=0，若业务支持搜索回收站需补充参数）
        QueryWrapper<AccountFileDO> queryWrapper = new QueryWrapper<AccountFileDO>()
                .eq("account_no", accountNo) // 权限过滤：仅当前用户的文件
                .like("file_name", search)    // 模糊匹配：文件名包含关键词
                .eq("del", 0)                 // 业务规则：仅查询未删除的文件（可根据需求调整）
                .orderByDesc("is_dir")        // 排序1：文件夹优先（is_dir=1为文件夹，0为文件）
                .orderByDesc("gmt_create")    // 排序2：最新创建的文件优先
                .last("limit 30");            // 性能保护：最多返回30条结果，避免大数据量查询

        // 2. 执行数据库查询
        List<AccountFileDO> accountFileDOS = accountFileMapper.selectList(queryWrapper);
        log.info("用户[{}]搜索完成，匹配到[{}]个文件", accountNo, accountFileDOS.size());

        // 3. DO转DTO（解耦数据库层与前端交互层，避免暴露敏感字段如del、gmt_modified）
        return SpringBeanUtil.copyProperties(accountFileDOS, AccountFileDTO.class);
    }

    /**
     * 批量获取文件下载URL（预签名URL模式）
     * 根据用户请求中的文件ID列表，筛选出「当前用户归属」且「非文件夹」的文件，
     * 调用MinIO生成带有效期的预签名URL，最终返回「文件名-下载URL」映射列表
     *
     * @param request 文件批量下载请求参数（含用户账号ID、待下载文件ID列表）
     * @return List<FileDownloadDTO> 批量下载结果（每个文件的名称+预签名下载URL）
     */
    @Override
    public List<FileDownloadDTO> batchDownloadUrl(FileBatchDownloadRequest request) {
        Long accountNo = request.getAccountNo();
        List<Long> fileIds = request.getFileIds();
        log.info("用户[{}]发起批量下载请求，文件ID列表：{}", accountNo, fileIds);

        // 1. 构建查询条件：用户归属+指定文件ID+非文件夹（仅支持文件下载，排除文件夹）+未删除
        QueryWrapper<AccountFileDO> queryWrapper = new QueryWrapper<AccountFileDO>()
                .eq("account_no", accountNo)          // 权限过滤：仅当前用户的文件
                .in("id", fileIds)                    // 范围过滤：仅指定ID的文件
                .eq("is_dir", FolderFlagEnum.NO.getCode()) // 类型过滤：仅非文件夹（NO=0）
                .eq("del", 0);                        // 业务规则：仅未删除的文件

        // 2. 执行查询，获取符合条件的用户文件关联记录
        List<AccountFileDO> accountFileDOS = accountFileMapper.selectList(queryWrapper);
        log.info("用户[{}]批量下载请求中，有效文件数量：{}（原始请求：{}个）",
                accountNo, accountFileDOS.size(), fileIds.size());

        // 3. 遍历有效文件，生成MinIO预签名下载URL
        List<FileDownloadDTO> fileDownloadDTOS = new ArrayList<>();
        for (AccountFileDO accountFileDO : accountFileDOS) {
            // 3.1 查询文件元数据（从File表获取MinIO中的objectKey，即文件在MinIO中的存储路径）
            FileDO fileDO = fileMapper.selectOne(
                    new QueryWrapper<FileDO>().eq("id", accountFileDO.getFileId())
            );
            if (fileDO == null) {
                log.warn("文件[{}]（用户[{}]）未查询到元数据，跳过生成下载URL",
                        accountFileDO.getFileId(), accountNo);
                continue;
            }

            // 3.2 调用MinIO引擎生成预签名下载URL（带有效期，配置中定义）
            String objectKey = fileDO.getObjectKey();
            String downloadUrl = minIOFileStoreEngine.getDownloadUrl(
                    minioConfig.getBucketName(),    // MinIO存储桶名（从配置获取）
                    objectKey,                      // 文件在MinIO中的唯一标识
                    minioConfig.getPreSingUrlExpireTime(), // URL有效期（如5分钟，从配置获取）
                    TimeUnit.MILLISECONDS                // 时间单位（与配置一致）
            );
            log.debug("文件[{}]（用户[{}]）生成预签名URL：{}",
                    accountFileDO.getFileName(), accountNo, downloadUrl);

            // 3.3 封装DTO（文件名+下载URL），添加到结果列表
            FileDownloadDTO downloadDTO = FileDownloadDTO.builder()
                    .fileName(accountFileDO.getFileName()) // 前端显示的文件名（保持用户上传时的名称）
                    .downloadUrl(downloadUrl)              // MinIO预签名下载URL
                    .build();
            fileDownloadDTOS.add(downloadDTO);
        }

        log.info("用户[{}]批量下载URL生成完成，共生成[{}]个有效URL",
                accountNo, fileDownloadDTOS.size());
        return fileDownloadDTOS;
    }

//    上传图片接口
    @Override
    public String ImgUpload(MultipartFile file) {
        String fileName = CommonUtil.getFilePath(file.getOriginalFilename());
        boolean upload = minIOFileStoreEngine.upload(minioConfig.getImgBucketName(), fileName, file);

        return minioConfig.getEndpoint() + "/" + minioConfig.getImgBucketName() + "/" + fileName;
    }

    @Override
    public AccountFileDO findParentIdByPhone(String phone) {
        AccountFileDO accountFileDO = accountFileMapper.selectOne(new QueryWrapper<AccountFileDO>().eq("phone", phone));
        return accountFileDO;
    }


}